﻿using lib.dmt.teltonika.server.Events;
using lib.dmt.teltonika.server.Parsers;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using ErrorEventArgs = lib.dmt.teltonika.server.Events.ErrorEventArgs;

namespace lib.dmt.teltonika.server.TCP
{
    public class TcpClientConnection : IDisposable
    {
        public static TimeSpan IDLE_TIMEOUT = TimeSpan.FromMinutes(5);

        private readonly TcpClient _tcpClient;
        private readonly NetworkStream _networkStream;
        private readonly CancellationTokenSource _cancellationTokenSource;
        private Task? _receiveTask;
        private Task? _idleMonitorTask;
        private bool _disposed;

        public string Id { get; }
        public EConnectionState State { get; private set; }
        public IPEndPoint? RemoteEndPoint { get; }
        public DateTime ConnectedTime { get; }
        public DateTime LastActivityTime { get; private set; }
        public long TotalBytesReceived { get; private set; }
        public long TotalBytesSent { get; private set; }

        public event EventHandler<DataByteReceivedEventArgs>? DataByteReceived;
        public event EventHandler<ErrorEventArgs>? Error;
        public event EventHandler? Disconnected;
        public event EventHandler<Code8EReceivedEventArgs>? DataCode8EReceived;
        public event EventHandler<TeltonikaRawByteReceivedEventArgs>? TeltonikaRawByteReceived;

        private Teltonika8EParser teltonika8EParser = new Teltonika8EParser();

        //private readonly ILoggerFactory _loggerFactory;
        private readonly ILogger? _logger;

        //public TcpClientConnection(ILoggerFactory? loggerFactory, TcpClient tcpClient)
        //{
        //    //this._loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
        //    this._logger = _loggerFactory.CreateLogger("AppLogger");

        //    _tcpClient = tcpClient ?? throw new ArgumentNullException(nameof(tcpClient));
        //    _networkStream = _tcpClient.GetStream();
        //    _cancellationTokenSource = new CancellationTokenSource();

        //    Id = Guid.NewGuid().ToString("N");
        //    RemoteEndPoint = (IPEndPoint)_tcpClient.Client.RemoteEndPoint!;
        //    ConnectedTime = DateTime.UtcNow;
        //    LastActivityTime = ConnectedTime;
        //    State = EConnectionState.Connected;
        //}

        public TcpClientConnection(ILogger? logger, TcpClient tcpClient)
        {
            this._logger = logger ?? throw new ArgumentNullException(nameof(logger));

            _tcpClient = tcpClient ?? throw new ArgumentNullException(nameof(tcpClient));
            _networkStream = _tcpClient.GetStream();
            _cancellationTokenSource = new CancellationTokenSource();

            Id = Guid.NewGuid().ToString("N");
            RemoteEndPoint = (IPEndPoint)_tcpClient.Client.RemoteEndPoint!;
            ConnectedTime = DateTime.UtcNow;
            LastActivityTime = ConnectedTime;
            State = EConnectionState.Connected;
        }

        public void StartReceiving()
        {
            if (_receiveTask != null && !_receiveTask.IsCompleted)
                return;

            _receiveTask = Task.Factory.StartNew(
                () => ReceiveLoopAsync(_cancellationTokenSource.Token),
                _cancellationTokenSource.Token,
                TaskCreationOptions.LongRunning,
                TaskScheduler.Default
            );

            // Start idle connection monitor
            _idleMonitorTask = Task.Factory.StartNew(
                () => IdleConnectionMonitorAsync(_cancellationTokenSource.Token),
                _cancellationTokenSource.Token,
                TaskCreationOptions.LongRunning,
                TaskScheduler.Default
            );
        }

        private async Task IdleConnectionMonitorAsync(CancellationToken cancellationToken)
        {
            try
            {
                while (!cancellationToken.IsCancellationRequested && _tcpClient.Connected)
                {
                    await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);

                    // Check if connection has been idle for too long
                    if (DateTime.UtcNow - LastActivityTime > IDLE_TIMEOUT)
                    {
                        _logger?.LogWarning($"[TcpClientConnection]; idle more than {IDLE_TIMEOUT.TotalMinutes} minutes, force to disconnect");
                        Disconnect("Idle connection timeout");
                        break;
                    }
                }
            }
            catch (OperationCanceledException)
            {
                // Cancellation requested, normal shutdown
            }
            catch (Exception ex)
            {
                Error?.Invoke(this, new ErrorEventArgs(this, ex));
            }
        }

        /// <summary>
        /// here to implement the receive processor
        /// </summary>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        private async Task ReceiveLoopAsync(CancellationToken cancellationToken)
        {
            const int bufferSize = 4096; // 8KB buffer
            byte[] data = new byte[bufferSize];

            _logger?.LogDebug($"[TcpClientConnection]; NEW Connection: {RemoteEndPoint}");

            try
            {
                var imei = await ReadIMEIAsync(_networkStream, cancellationToken);
                if (string.IsNullOrEmpty(imei) == true)
                {
                    //incorrect IMEI
                    _logger?.LogError($"[TcpClientConnection]; failed to read the IMEI value");
                    return;
                }
                LastActivityTime = DateTime.UtcNow;

                while (!cancellationToken.IsCancellationRequested && _tcpClient.Connected)
                {
                    //var bytesRead = await _networkStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);

                    //if (bytesRead == 0)
                    //{
                    //    // Connection closed by client
                    //    break;
                    //}

                    //LastActivityTime = DateTime.UtcNow;
                    //TotalBytesReceived += bytesRead;

                    //// Copy the received data to a new array
                    //var receivedData = new byte[bytesRead];
                    //Buffer.BlockCopy(buffer, 0, receivedData, 0, bytesRead);

                    //read from the stream with MAX size
                    int numBytesRead = _networkStream.Read(data, 0, data.Length);

                    TotalBytesReceived += numBytesRead;

                    //// Raise data received event
                    DataByteReceived?.Invoke(this, new DataByteReceivedEventArgs(this, data, numBytesRead));

                    //create a read-size buffer and copy from orignal buffer
                    byte[] aData = new byte[numBytesRead];
                    Buffer.BlockCopy(data, 0, aData, 0, numBytesRead);

                    //if the buffer size is ZERO, disconnect the TcpClient
                    if (aData.Length == 0)
                    {
                        _logger?.LogError($"[TcpClientConnection]; receive ZERO length data from IMEI:{imei}");
                        break;
                    }

                    //no matter the codec, rise the raw byte event
                    TeltonikaRawByteReceived?.Invoke(this, new TeltonikaRawByteReceivedEventArgs(new TeltonikaRawByteRecord(imei, aData)));

                    //handle the Codec8-Extended protocol
                    if (aData[8] == 0x8e)
                    {
                        LastActivityTime = DateTime.UtcNow;
                        //decode 8E
                        var avlPacket = teltonika8EParser.ParseAVLDataPacket(aData, imei);
                        if (avlPacket == null)
                        {
                            _logger?.LogError($"[TcpClientConnection]; failed to parse the receive raw bytes from stream. RAW:[{BitConverter.ToString(aData)}]");
                        }
                        else
                        {
                            //maching the name for each IOProperty
                            //for (int i = 0; i < avlPacket.AVLData.Count; i++)
                            //{
                            //    for (int j = 0; j < avlPacket.AVLData[i].IO.OneByteIO.Count; j++)
                            //    {
                            //        avlPacket.AVLData[i].IO.OneByteIO[j].Name = TeltonikaAVLParametersID.GetName((int)avlPacket.AVLData[i].IO.OneByteIO[j].Id);
                            //    }
                            //    for (int j = 0; j < avlPacket.AVLData[i].IO.TwoBytesIO.Count; j++)
                            //    {
                            //        avlPacket.AVLData[i].IO.TwoBytesIO[j].Name = TeltonikaAVLParametersID.GetName((int)avlPacket.AVLData[i].IO.TwoBytesIO[j].Id);
                            //    }
                            //    for (int j = 0; j < avlPacket.AVLData[i].IO.FourBytesIO.Count; j++)
                            //    {
                            //        avlPacket.AVLData[i].IO.FourBytesIO[j].Name = TeltonikaAVLParametersID.GetName((int)avlPacket.AVLData[i].IO.FourBytesIO[j].Id);
                            //    }
                            //    for (int j = 0; j < avlPacket.AVLData[i].IO.EightBytesIO.Count; j++)
                            //    {
                            //        avlPacket.AVLData[i].IO.EightBytesIO[j].Name = TeltonikaAVLParametersID.GetName((int)avlPacket.AVLData[i].IO.EightBytesIO[j].Id);
                            //    }
                            //    for (int j = 0; j < avlPacket.AVLData[i].IO.XBytesIO.Count; j++)
                            //    {
                            //        avlPacket.AVLData[i].IO.XBytesIO[j].Name = TeltonikaAVLParametersID.GetName((int)avlPacket.AVLData[i].IO.XBytesIO[j].Id);
                            //    }
                            //}

                            //create ACK message
                            var ack = teltonika8EParser.CreateAcknowledgement(avlPacket.NumberOfRecords1);

                            //_logger?.LogTrace($"[TcpClientConnection/{imei}/CODEC_8E/{Task.CurrentId}]; RECV; OK; RecordCount={avlPacket.NumberOfRecords1}; FirstRecordLocalTime:{avlPacket.AVLData.FirstOrDefault()?.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")}");

                            //send the ACK message
                            await _networkStream.WriteAsync(ack, 0, ack.Length, cancellationToken);
                            TotalBytesSent += ack.Length;

                            //fire event if Code8
                            DataCode8EReceived?.Invoke(this, new Code8EReceivedEventArgs(imei, JsonConvert.SerializeObject(avlPacket)));
                        }
                    }

                    else if (aData[8] == 0x0C)
                    {
                        LastActivityTime = DateTime.UtcNow;
                        //decode Codec12
                        byte len = aData[14];
                        if (len > 0)
                        {
                            byte[] rawRecv = new byte[len];

                            //copy the useful infomration from buffer
                            Buffer.BlockCopy(aData, 15, rawRecv, 0, len);
                            var str = System.Text.Encoding.Default.GetString(rawRecv);
                            _logger?.LogTrace($"[TcpClientConnection/{imei}/CODEC_12]/{Task.CurrentId}; RECV; OK; rawMessage={str}, length={rawRecv.Length}");
                        }
                    }
                }
            }
            catch (OperationCanceledException)
            {
                // Cancellation requested, normal shutdown
            }
            catch (Exception ex)
            {
                Error?.Invoke(this, new ErrorEventArgs(this, ex));
            }
            finally
            {
                Disconnect("Receive loop ended");
            }
        }

        public async Task SendAsync(byte[] data, CancellationToken cancellationToken = default)
        {
            if (data == null) throw new ArgumentNullException(nameof(data));
            if (State != EConnectionState.Connected) return;

            try
            {
                await _networkStream.WriteAsync(data, 0, data.Length, cancellationToken);
                TotalBytesSent += data.Length;
                LastActivityTime = DateTime.UtcNow;
            }
            catch (Exception ex)
            {
                Error?.Invoke(this, new ErrorEventArgs(this, ex));
                throw;
            }
        }

        public void Disconnect(string reason = "Requested")
        {
            if (State == EConnectionState.Disconnected || State == EConnectionState.Disconnecting)
                return;

            State = EConnectionState.Disconnecting;
            _cancellationTokenSource.Cancel();

            try
            {
                _tcpClient?.Close();
            }
            catch
            {
                // Ignore errors during close
            }

            State = EConnectionState.Disconnected;
            Disconnected?.Invoke(this, EventArgs.Empty);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    Disconnect("Disposed");
                    _cancellationTokenSource?.Dispose();
                    _tcpClient?.Dispose();
                }
                _disposed = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }


        #region 
        private async Task<string?> ReadIMEIAsync(NetworkStream stream, CancellationToken ct)
        {
            var lengthBytes = new byte[2];
            if (await stream.ReadAsync(lengthBytes, 0, 2, ct) != 2) return null;

            var length = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(lengthBytes));
            var imeiBytes = new byte[length];
            if (await stream.ReadAsync(imeiBytes, 0, length, ct) != length) return null;

            //ACK IMEI
            var imei = Encoding.ASCII.GetString(imeiBytes);
            await stream.WriteAsync(new byte[] { 0x01 }, 0, 1, ct);
            return imei;
        }
        #endregion
    }
}
